Skip to content

IronRuby 4.0: Ruby 4.0 Support - #13

Open
Largo wants to merge 1096 commits into
IronLanguages:masterfrom
Largo:master
Open

Largo wants to merge 1096 commits into
IronLanguages:masterfrom
Largo:master

Conversation

@Largo

@Largo Largo commented Sep 30, 2026

Copy link
Copy Markdown

Hi, I updated IronRuby to be Ruby 4.0 compatible. It's using prism now. I managed to create working dlls as well. I don't expect you to merge this huge change, but I would like to make you aware, that there is a working ruby 4.0 version.

Largo and others added 30 commits September 19, 2026 14:41
At startup $" listed the prelude's own files (ruby4.rb, argf.rb, thread.rb,
complex18.rb, rational18.rb); requiring 'complex' or 'rational' loaded the
1.9 compatibility files (complex.rb even replaced Math with CMath), and
'ruby2_keywords' raised LoadError. Once the core prelude is loaded its files
are kept out of $" (still counted as loaded) and $" lists enumerator.so,
thread.rb, fiber.so, rational.so, complex.so, pathname.so, ruby2_keywords.rb
and set.rb; a feature listed by name is loaded for a require of that name.
Set and Pathname are autoloaded by file path, and Kernel#Pathname loads the
class on first use.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hread.new's result

:c_call and :c_return were never delivered. While a TracePoint wants them,
the call-site rules of library (C#) methods report them around the call, at
the caller's location; switching them on or off rebinds every rule, so a
rule bound without tracing costs nothing. A core-library method written in
Ruby (ruby4.rb) called from a program reports :c_call (not yet :c_return).
Calls made by the core library itself, and TracePoint's own methods, report
nothing, as in MRI.

With :thread_begin/:thread_end traced, a new thread waits (up to 100ms,
looking running) until its creator blocks, as it would under MRI's GVL, so
its hooks see the variable Thread.new's result was stored in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
instance_eval("\n\nraise", "b_file", -100) reported b_file:3: a SourceLocation
(and the CLR's debug info) can't start before line 1, so the code was
compiled from 1 and the given line dropped. The difference now travels in
the eval's document name, after a marker, and is taken back out where the
file name is read: backtrace frames, __FILE__ and __LINE__, and the
compiler's source path. (TracePoint :line events in such an eval still
count from 1.)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…thread

MRI keeps the value a file assigns to its autoloaded constant in the autoload
until the file has finished: the loading thread sees it, every other thread
still sees the pending autoload (Module#autoload? is the path, a reference
waits for the load, const_source_location is the autoload's). The value used
to replace the autoload at once, so another thread could get a class whose
body was still running. The value and its location are now kept by the
AutoloadedConstant and published when the load ends, before waiting threads
are woken.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… no extra newline

prism leaves some warnings (regex literal in condition) out of -e code when
told the code came from -e, as MRI does. And the main script is parsed before
the prelude's `extend self' on Warning, so DispatchWarning resolved Kernel#warn,
which appended a second newline; it now falls back to writing the message.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MRI 3.4+ shows a frame of a Ruby-defined core method (<internal:kernel>)
with the location of the code that called it, as for a C method. Backtraces
(caller, caller_locations, exception and thread backtraces) now do the same
for methods in ruby4.rb, argf.rb and thread.rb, and leave out blocks and
file-level code there. Their Method#source_location is <internal:module>.
Kernel#tap is written in Ruby, as MRI's is; warn(uplevel:) skips <internal:
frames. Kernel#Pathname's stub no longer warns about a useless ::Pathname.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A source name that merely contains U+0001 (IronRuby.Tests Backtrace7) lost
everything from it on in backtraces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…_call inside Ruby-written core

- A buffered write or flush to STDOUT/STDERR whose reader is gone raises
  SignalException SIGPIPE (MRI's FMODE_SIGNAL_ON_EPIPE), so `ir -pe '' | head`
  dies quietly of SIGPIPE; syswrite still raises Errno::EPIPE.
- -00 is paragraph mode and -0777 (any value >= 0400) makes $/ nil; the latter
  crashed at startup. A separator byte above 0x7F no longer asserts.
- ARGF.gets/readlines pass chomp: through to IO#gets.
- TracePoint c_call locates a library call with the raw backtrace: since core
  methods written in Ruby report their caller's location, their own inner
  calls (Array#max calling empty?) were being delivered as the program's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ces, mutex fiber reclaim, delegate undef guard

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add Src/StdLib/ruby/4.0/fiddle.rb implementing Fiddle::Handle on top of
System.Runtime.InteropServices.NativeLibrary (real dlopen/dlsym/dlclose):
Handle.new with DLError on missing libraries (dlerror text preserved),
sym/sym_defined?/close/close_enabled?/file_name/to_i, DEFAULT and NEXT
handles, Handle.sym, Fiddle.dlopen, last_error, RTLD_/TYPE_/SIZEOF_
constants. Shadows the stale 1.9.1 fiddle.rb that failed to load
(require 'fiddle.so').

spec/library/fiddle/handle/initialize_spec.rb: 1 example, 0 failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d#backtrace

OpenSSL::X509::Certificate, ExtensionFactory and Store are now written in Ruby
on System.Security.Cryptography.X509Certificates: CertificateRequest builds and
signs a certificate field by field the way MRI's does, and Store#verify runs an
X509Chain whose trust anchors are the self-signed certificates in the store, so
a certificate signed by an expired issuer in the store fails the way OpenSSL's
does rather than being trusted on its own.  PKey::RSA grows enough to generate a
key pair and hand out its public half.  The C# Certificate stub it replaces
could only wrap an already-parsed X509Certificate and its RubyConstructor left
no way to define #initialize in Ruby, so it is gone.

Socket#connect honours IO#timeout, which is what the accessor is for here: the
connect is started non-blocking and a wait that runs out is IO::TimeoutError.

A thread parked in accept(2) is inside a native call with no Ruby frame of its
own, so Thread#backtrace stopped at the call site where MRI shows the method
itself; the blocking socket helpers now name the method they are in, the way a
sleeping thread already reported Kernel#sleep.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
File::APPEND and the Fcntl::O_* constants were IronRuby's own IOMode bits,
which are the MSVCRT numbers; on Linux fcntl(2) neither reports nor accepts
them, and IO#fcntl(F_GETFL) had to OR the append bit back in by hand to look
right. IOModeNative now holds the platform's own open(2) values and converts
between them and IOMode, so File::Constants, Fcntl and everything that takes a
numeric mode (File.open, IO#reopen, Kernel#open, StringIO.new, the :flags
option) agree with the kernel. Linux has no O_BINARY or O_SHARE_DELETE, so
File::BINARY and File::SHARE_DELETE are 0 there, as in CRuby.

open(2)'s ENXIO - /dev/tty with no controlling terminal - came back as a bare
IOError, because .NET raises a plain IOException for it. Errno::ENXIO is a
class of its own now and OpenFileStream raises it, which is what CRuby does.

IO.pipe's write end and IO.popen's IO report sync, as MRI's do.

mkmf could not be required at all: RbConfig had no MAKEFILE_CONFIG, no
CROSS_COMPILING, none of the keys mkmf reads while it loads, and no rubyhdrdir.
The toolchain keys are empty - IronRuby was not built by a C compiler and
cannot build a C extension - and rubyhdrdir names a header this tree now ships
whose whole content is an #error saying so, since mkmf aborts before defining
anything if it cannot find ruby/ruby.h.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The objspace library (Src/StdLib/ironruby/objspace.rb, objspace/trace.rb) with
the runtime it needs: dump/dump_all, memsize_of/memsize_of_all,
reachable_objects_from, and allocation tracing.

MRI gets all of this from walking its heap and from a per-object allocation
record; neither exists on .NET, so:

 * dump(obj) describes the object it is given - type, class, size, value,
   references, ivars - in MRI's JSON shape, with #object_id where MRI prints an
   address and without the heap-internal fields (shape_id, slot_size, flags).
 * dump_all and memsize_of_all walk what ObjectSpace knows of: every module and
   class, plus the strings, arrays, hashes and Ruby objects created since the
   library was required. `require "objspace"` switches that recording on
   (ObjectTracking), which is the one cost the library imposes - nothing is
   recorded before it, and objects of other types are not recorded at all.
 * memsize_of estimates the managed size (header + payload), for comparing
   objects of the same kind; 0 for an immediate, as in MRI.
 * trace_object_allocations takes each allocation's location from the
   TracePoint :line hook of the statement that allocated, rather than building a
   stack trace per object: tracing forces the :line event on and the hook leaves
   its file and line for the next allocation to pick up. The class path and
   method id are the allocating method's, nil in a block or at the top level.

The allocation points are the constructors of MutableString, RubyArray, Hash and
RubyObject, each guarded by one static field read when the library is not in use.
reachable_objects_from_root is not implemented: there is no root set to answer
with.

Coverage gains :methods: a `def' run while the measurement is set up records
[class, name, start line, start column, end line, end column], and the compiled
body's prologue counts the calls, so a method that is never called is still
reported with 0. Branch coverage is still not measured, but a result asked for
it now reports an empty set per file rather than leaving the key out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ing and bytes/chars

Startup and the cold paths the core library runs at load time:

- Module#const_set captured its source location with a full CLR stack walk
  (`new StackTrace(true)`, which reads the PDB of every assembly on the stack)
  costing ~1ms per call. The interpreter's own frame chain already knows where
  we are, so read the innermost Ruby frame from there and keep the stack walk
  as a fallback. Errno's 130 classes and the Encoding constant table are set up
  with hundreds of const_set calls at every start.
- A CLR stack walk asked for file and line info on every raise as well. Only a
  debug-mode run can use it: a Ruby frame carries its own file and line, and the
  frames that would answer from a PDB are IronRuby's own, which never reach the
  Ruby backtrace. `ir -e ''` goes from ~1.61s to ~1.35s.

Collections and strings:

- Array gets its own #min, #max, #sort_by, #each_with_object and #zip in C#
  (IListOps) instead of inheriting Enumerable's, which are written against #each
  and cost a Proc, a BlockParam and an argument-packing array per element.
  #min/#max are ~8x faster, #sort_by ~6x, #zip ~5x.
- Protocols.ConvertCompareResult uses an Integer <=> result directly instead of
  asking it how it compares to 0 through two more call sites, as MRI's rb_cmpint
  does. That is two of the three dynamic calls per comparison in #sort, #min,
  #max and #sort_by.
- Array#compact walks a RubyArray directly and presizes the result.
- Case mapping takes an ASCII fast path: over ASCII the Unicode mappings are the
  ASCII ones, and the general path costs a binary search over the special-casing
  tables plus a Rune round trip per code point. String#upcase is ~7x faster.
- String#bytes and String#chars build the Array in C# instead of draining an
  Enumerator from ruby4.rb (~4x).
- Struct#initialize short-circuits the common "one positional value per member"
  call before any of the keyword_init handling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Util/bench/{bench.rb,run.sh} time IronRuby against CRuby on 50 micro and medium
benchmarks (calls, blocks, ivars, constants, arithmetic, control flow, string
interpolation, exceptions, method_missing, super, deep hierarchies, fibers),
best-of-N per process with start-up excluded, and print the ratios.

Three things the profiles pointed at:

RubyMethodScope's constructor composed its debug name - "method " + name - on
every single method call.  SetDebugName is [Conditional("DEBUG")], but the
concatenation is an argument, so the Debug build (which is what ir.sh runs) paid
a string allocation per call.  The name is now built on demand in ToString.
Empty-call benchmarks are ~25% faster, ivar reads through a method ~38%.

Ruby backtraces never need the CLR's file and line numbers: a Ruby frame carries
its file and line in the name of the generated method, and an interpreted frame
has its own debug info.  Asking for them made .NET open and parse a PDB per
assembly on every raise.  Only -X:ExceptionDetail and -X:Debug, which do report
CLR frames, ask for them now; raise/rescue is ~26% faster and the backtraces are
byte-identical.

RubyOps.CanRescue looked the exception's RubyExceptionData up twice per rescue,
and that store is a linear scan of Exception.Data.

ir.sh grew IR_CONFIG, so that IR_CONFIG=Release runs the optimized build; Debug
stays the default because that is what the build and test scripts produce.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MRI re-reads the receiver's length on every step, so a block that grows the
array is iterated into (spec/core/array/shared/iterable_and_tolerating_size_increasing,
reached through #sort_by!). The C# #sort_by added in the previous commit read
the length once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
#	Src/Ruby/Runtime/RubyExceptionData.cs
#	Src/Ruby/Runtime/RubyStackTraceBuilder.cs
CRuby 4.0 implements Ripper on top of prism (Prism::Translation::Ripper),
and the same two prism primitives are enough here:

  Ripper.__lex__   prism's token stream (pm_serialize_lex), each token
                   carrying the MRI lexer state it left behind
  Ripper.__parse__ prism's tree, handed over generically as a Hash per
                   node keyed by prism's own field names

Both live in Src/Libraries/Ripper/RipperOps.cs; everything Ripper-shaped
- MRI's event names, its s-expressions, its lexer-state objects - is
Ruby, in Src/StdLib/ironruby/ripper.rb and ripper/sexp_builder.rb. The
tree is generic rather than typed so that covering another node never
needs a C# change.

Ripper.sexp and Ripper.sexp_raw agree with CRuby 4.0.6 byte for byte on
every file of Src/StdLib/ruby/4.0 (187), mspec/lib (130) and
spec/language (162). Ripper.lex agrees on 52991 of 53142 tokens of
mspec/lib; what is left is the lexer state reported on :on_sp and
:on_comment in positions where MRI's parser feeds a state back into the
lexer before the whitespace is scanned - the same limitation prism's own
lex_compat.rb has.

The prism token-type -> Ripper event table is vendored from ruby/prism's
lex_compat.rb and Ripper.slice / Ripper::TokenPattern from CRuby's
lib/ripper/lexer.rb, both with their licence notices.

Not implemented: Ripper's event-callback API (Ripper.new(src).parse).

The assembly reference between IronRuby.Libraries and IronRuby.Prism is
flipped so the Ripper library can call into prism; Prism never used
anything from Libraries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Largo and others added 30 commits October 1, 2026 22:40
- The version is 4.0.0 (CurrentVersion.cs, CurrentVersion.props): the first stable
  release since 1.1.3 in 2011.
- The README opens with the social card, the download links for v4.0.0, the
  launch video's poster, the highlights, and a screenshot of Samples/Notepad
  editing its own source. The CI badge follows master, not the old modernize.
- media/social: the social card (card.html) and its renders, 1600x900 for posts
  and the README, 1600x800 for GitHub's social preview.
- media/launch: the launch video, a HyperFrames composition (1920x1080, 55 s),
  with its fonts and the Notepad screenshot. Every figure in it is the README's.
- release.yml renders the video on Ubuntu (lint must pass, check only warns)
  and attaches it to the release, uses Docs/releases/<tag>.md as the notes, and
  drives the Notepad test from the unpacked Windows archive, which now carries
  Samples/.
IronRuby 4.0.0: version, downloads, social card and launch video
…easer

GitHub does not play an MP4 in a README unless it was uploaded through its web
editor, so the README shows media/launch/teaser.webp (960x540, 10 fps, 1.3 MB:
the title, the irb session, .NET and the Notepad, the end card) and links it,
and a line below it, to the full video on the v4.0.0 release.
README: the launch video plays inline
Grieg's piece, played by the Czech National Symphony Orchestra for Musopen and
marked public domain (CC PDM 1.0) on classicals.de, which is where
media/launch/tools/fetch-music.sh downloads it from before a render; the
repository does not carry it. The quiet opening plays under the 2011 scene,
the film cuts into the full orchestra at the title, and the piece's last chord
is meant to fall on the end card. --profile prints the recording's loudness
every half second, to place that cut without listening.

release.yml fetches the music before the render, and run by hand with
video_for: <tag> renders only the video and replaces it, and its SHA256SUMS
line, on a release that is already out.

The SHA-256 of the download and the offset of the cut are not pinned yet: this
change's own CI run reports both.
From the first run's fetch-music.sh --profile: the recording is 154.1 s, the
opening near -40 LUFS, the full orchestra entering at 101 s, a general pause at
147.5-148.3 s and the final chords at 148.4-150.4 s. The film now cuts in at
104.1 s with the title, which puts the pause just before the end card and the
final chord on it (49.6 s); the opening under 2011 plays 11 dB up.

The zip's SHA-256 is pinned, and the script skips the __MACOSX resource fork
the zip also carries.
A volume tween sets the media element's volume, which only goes from 0 to 1:
tweening the opening to 3.5 failed the render (IndexSizeError), and hyperframes
then waited until the job's 45-minute limit. The opening keeps its +11 dB as a
static data-volume, which the mixer applies, and needs no fade: the recording
begins in silence and the cut into the orchestra covers its end.
Launch video: In the Hall of the Mountain King
PosixSpawn hard-coded Linux's O_CLOEXEC (0x80000). macOS's pipe2 rejects
that flag with EINVAL, so every pipe to a child failed there: backticks,
%x(), IO.popen, IO.pipe handed to spawn, and Open3 all raised
Errno::EINVAL "pipe". system and spawn without a pipe were unaffected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A BSD sockaddr starts with a one-byte sa_len and then a one-byte
sa_family; Linux and Windows start with a 16-bit sa_family. Reading 16
bits on macOS gave (family << 8) | length - 528 for AF_INET, 7708 for
AF_INET6 - so every address the kernel handed back (getsockname, accept,
recvfrom) raised "unknown address family", and puma could not bind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
write_nonblock reached sockets through the buffered Write, and
SocketStream.Write drops the count Socket.Send returns. On a
non-blocking socket Send takes only what fits in the kernel buffer, so
write_nonblock reported the whole string written when only part of it
was, and the rest was lost. Puma trusts that count, so a response
larger than the socket buffer arrived truncated and the client waited
for the missing bytes until it timed out.

DescriptorStream already had the right primitive, WriteNonBlocking; it
becomes an interface SocketStream implements too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IO#write_nonblock on a socket returns what the kernel took
Socket: read a packed sockaddr's family where macOS and the BSDs keep it
Process: O_CLOEXEC is 0x1000000 on macOS, so pipes to a child open there
Util/socket-write-nonblock-check.rb fills a socket's send buffer with
write_nonblock until it refuses and checks that the bytes it claimed are the
bytes the peer gets; it fails rather than hangs if write_nonblock never refuses.
That is the companion test #14 asked for. CI runs it on Linux and Windows.

On Linux the send buffer fills part-way through a write, which is where the
bug lost bytes. On Windows the check passed before #14 as well (measured on the
4.0.0 build: claimed 4,000,000, received 4,000,000): Winsock takes a
non-blocking send whole or refuses it, with default and with 16 KB buffers, so
there it guards that what is claimed keeps arriving.
CI: write_nonblock on a socket claims only what the peer receives (#14)
The fixes since 4.0.0: pipes to child processes on macOS (#12), socket
addresses on macOS and the BSDs (#13), and IO#write_nonblock on sockets
claiming more than the kernel took (#14), with its CI check (#15).
Docs/releases/v4.0.1.md is the release text.

The display and file versions are 4.0.1; AssemblyVersion stays 4.0.0.0 across
4.0.x, so what was built against 4.0.0 binds to 4.0.1. The README links the
4.0.1 downloads and says what is known about macOS now: built, not tested in CI,
and fixed there by #12 and #13.
SQLitePCLRaw's libe_sqlite3.so exports SQLite's API and is linked without
-Bsymbolic, so once another SQLite is in the global symbol scope (a plugin
loaded RTLD_GLOBAL, libvips's vips-openslide for one, or LD_PRELOAD) its calls
into itself bind to that copy and the first sqlite3_open_v2 segfaults.

Util/build-e_sqlite3.sh builds the same SQLite, 3.53.3 from sqlite.org's
amalgamation (checked against its source id), with SQLitePCLRaw's gcc flags
and the compile options the 2.1.13 build reports, plus -Wl,-Bsymbolic.
package-release.sh swaps it into the Linux archive and checks, with
Util/sqlite-options.rb, that SQLite reports the same version, source id and
compile options before and after, and the same again with the system's
libsqlite3 preloaded; it also logs what the shipped library does in that case.
The zip's SHA-256 is not pinned yet: this change's own run reports it.
As the first run reported it (release run 36980600162). That run also showed
the fix doing its job: the shipped libe_sqlite3.so segfaulted with the
system's libsqlite3 preloaded, the rebuilt one reported the same version,
source id and compile options and kept answering as 3.53.3.
With TZ unset, RubyTimeZone.Machine() reads /etc/localtime, and on Windows,
which has none, fell back to a fixed offset taken from
TimeZoneInfo.Local.BaseUtcOffset. That is the zone's standard offset, so every
local Time was an hour off all summer: Time.now on 2026-10-12 in
Europe/Zurich ("W. Europe Standard Time") said 20:20:46 +0100 while
DateTime.Now said 21:20:46, and Time.at, Time.local/mktime/new, #localtime and
dst? were wrong the same way.

The machine zone is now backed by TimeZoneInfo.Local itself and asked for each
instant (GetUtcOffset, IsDaylightSavingTime), so it carries the zone's DST and
historical rules. Wall-clock readings go through the same GetOffsetForWallClock
as the zoneinfo-backed zones, so a reading in the spring gap or the autumn
overlap resolves as in MRI. Time#zone reports the zone's DaylightName during
daylight saving time and its StandardName otherwise.

Nothing changes where zoneinfo exists: /etc/localtime and the TZ ids and POSIX
rules still go through TzFile.

Util/time-local-matrix.rb prints utc_offset, dst? and the wall clock for the
machine's zone across a year, through both EU and US changes and for the gap
and overlap readings. On Windows 11 in W. Europe Standard Time, against CRuby
4.0.1 on the same machine: 102 of 299 lines differed before, none after; with
TZ=CET-1CEST,M3.5.0,M10.5.0/3 (the TzFile path) the output is identical too.
TZ=America/Los_Angeles (or Europe/Zurich, or a Windows id such as
"W. Europe Standard Time") is looked up in the zoneinfo directories, which
Windows does not have, then parsed as a POSIX rule, which it is not, so the
Time came out in UTC. On Windows, an id with no zoneinfo file is now handed to
TimeZoneInfo.FindSystemTimeZoneById, which knows the Windows ids and, through
ICU, the Olson ones; POSIX strings such as "PST8:00:00" or
"CET-1CEST,M3.5.0,M10.5.0/3" are not ids and still take the POSIX path.

Linux and macOS are unchanged: there the lookup stays zoneinfo, then POSIX.

spec/core/time on Windows: 639 examples, 1 failure (Time#dst? in
America/Los_Angeles) before this and the previous commit; 0 failures after.
Together they also fix File::Stat#atime/#ctime/#mtime/#birthtime there, whose
specs compare with local Times.
_dump wrote the zone name with MutableString.CreateAscii, so on a Windows whose
zone names are localized ("Mitteleuropäische Zeit") Marshal.dump of any local
Time raised EncoderFallbackException. It now encodes the name as Time#zone
already does: ASCII when it is, the path encoding otherwise.

spec/core/marshal on a German Windows: 6 errors before (all "for a Time"),
none after.
… finds its binstub

The Windows twin of the ir.sh change in #18. RubyGems writes a gem's
binstubs to Gem.bindir, which for IronRuby is the directory ir.exe itself
is in (RbConfig's bindir). ir.cmd only put Src\StdLib\bin on RUBYPATH, so
`ir.cmd -S <gem executable>` after `igem.cmd install <gem>` skipped the
binstub RubyGems had just written and took whatever came first on PATH -
a host Ruby's command of the same name, if there was one.

The directory of ir.exe now comes after Src\StdLib\bin and before an
existing RUBYPATH, the same order ir.sh uses.
CI builds libprism from prism's main branch, and since ruby/prism#4255
(attach_comments, merged 2026-10-12) every build job has failed in the
smoke test with "Unknown prism node type: 0 (TypeError)" - master, #17,
#18 and #19 alike. That change altered prism's serialization in two
places without moving the serialization version off 1.9, so the version
check in PrismLoader could not catch it:

- the options blob has a new attach_comments byte after freeze. The blob
  IronRuby wrote was one byte short, so prism read the scope count from
  the wrong offset;
- the serialized header has a comment-attachment list after the comments
  (node id, count, comment indices). PrismLoader read its count as the
  magic comment count and walked off from there.

PrismParser.BuildOptionsData writes the byte (0: IronRuby never asks for
attachments) and PrismLoader.Load skips the list. PrismLex only reads the
token stream, which ends before the header, so it needs nothing.

The rest of the revision comes along so that every piece names the same
prism commit, as Src/Prism/README.md asks:

- Src/Prism/Generated is regenerated from cd76b0d1's config.yml:
  SymbolNode#value_loc became the non-optional content_loc (the bridge
  reads neither), and a WORDS_SEP_IMPLICIT token was added after
  WORDS_SEP. Ripper looks tokens up by name and skips the ones it has no
  event for, which is what prism's own lex_compat does with this one.
- Src/StdLib/ruby/4.0/prism is prism's lib/ at cd76b0d1, generated files
  included; prism.rb keeps IronRuby's backend branch.
  parse_result/comments.rb is gone upstream - attaching comments moved
  into libprism.
- Src/StdLib/ironruby/prism/ironruby.rb follows ffi.rb: attach_comments
  is a known option and is dumped after freeze, and the *_file APIs take
  a Pathname (to_path / to_str) the way PrismSource.with_file now does.
- release.yml's PRISM_REF and the README build instructions move to
  cd76b0d1.
prism: follow ruby/prism cd76b0d1 (attach_comments) - fixes red CI
Time: follow daylight saving time on Windows
ir.cmd: put RubyGems' bindir on RUBYPATH, so `ir -S <gem executable>` finds its binstub
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants